Passed
Branch v10.2.x (3351e3)
by Rafael S.
02:34
created

WaveFileCreator.createPCMHeader_   A

Complexity

Conditions 1

Size

Total Lines 20
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 20
rs 9.5
c 0
b 0
f 0
cc 1
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileCreator class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { WaveFileParser } from './wavefile-parser';
31
import { interleave, deInterleave } from './parsers/interleave';
32
import { validateNumChannels } from './validators/validate-num-channels'; 
33
import { validateSampleRate } from './validators/validate-sample-rate';
34
import { packArrayTo, unpackArrayTo, packTo, unpack } from 'byte-data';
35
36
/**
37
 * A class to read, write and create wav files.
38
 * @extends WaveFileParser
39
 * @ignore
40
 */
41
export class WaveFileCreator extends WaveFileParser {
42
43
  constructor() {
44
    super();
45
    /**
46
     * The bit depth code according to the samples.
47
     * @type {string}
48
     */
49
    this.bitDepth = '0';
50
    /**
51
     * @type {{be: boolean, bits: number, fp: boolean, signed: boolean}}
52
     * @protected
53
     */
54
    this.dataType = {bits: 0, be: false, signed: false, fp: false};
55
    /**
56
     * Audio formats.
57
     * Formats not listed here should be set to 65534,
58
     * the code for WAVE_FORMAT_EXTENSIBLE
59
     * @enum {number}
60
     * @protected
61
     */
62
    this.WAV_AUDIO_FORMATS = {
63
      '4': 17,
64
      '8': 1,
65
      '8a': 6,
66
      '8m': 7,
67
      '16': 1,
68
      '24': 1,
69
      '32': 1,
70
      '32f': 3,
71
      '64': 3
72
    };
73
  }
74
75
  /**
76
   * Set up the WaveFileCreator object based on the arguments passed.
77
   * Existing chunks are reset.
78
   * @param {number} numChannels The number of channels.
79
   * @param {number} sampleRate The sample rate.
80
   *    Integers like 8000, 44100, 48000, 96000, 192000.
81
   * @param {string} bitDepthCode The audio bit depth code.
82
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
83
   *    or any value between '8' and '32' (like '12').
84
   * @param {!Array|!TypedArray} samples The samples.
85
   * @param {?Object} options Optional. Used to force the container
86
   *    as RIFX with {'container': 'RIFX'}
87
   * @throws {Error} If any argument does not meet the criteria.
88
   */
89
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
90
    // reset all chunks
91
    this.clearHeaders();
92
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
93
  }
94
95
  /**
96
   * Set up the WaveFileParser object from a byte buffer.
97
   * @param {!Uint8Array} wavBuffer The buffer.
98
   * @param {boolean=} samples True if the samples should be loaded.
99
   * @throws {Error} If container is not RIFF, RIFX or RF64.
100
   * @throws {Error} If format is not WAVE.
101
   * @throws {Error} If no 'fmt ' chunk is found.
102
   * @throws {Error} If no 'data' chunk is found.
103
   */
104
  fromBuffer(wavBuffer, samples=true) {
105
    super.fromBuffer(wavBuffer, samples);
106
    this.bitDepthFromFmt_();
107
    this.updateDataType_();
108
  }
109
110
  /**
111
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
112
   * The return value of this method can be written straight to disk.
113
   * @return {!Uint8Array} A wav file.
114
   * @throws {Error} If bit depth is invalid.
115
   * @throws {Error} If the number of channels is invalid.
116
   * @throws {Error} If the sample rate is invalid.
117
   */
118
  toBuffer() {
119
    this.validateWavHeader_();
120
    return super.toBuffer();
121
  }
122
123
  /**
124
   * Return the samples packed in a Float64Array.
125
   * @param {?boolean} interleaved True to return interleaved samples,
126
   *   false to return the samples de-interleaved. Defaults to false.
127
   * @return {!Array|!TypedArray} the samples.
128
   */
129
  getSamples(interleaved=false) {
130
    /**
131
     * A Float64Array created with a size to match the
132
     * the length of the samples.
133
     * @type {!Array|!TypedArray}
134
     */
135
    let samples = new Float64Array(
136
      this.data.samples.length / (this.dataType.bits / 8));
137
    // Unpack all the samples
138
    unpackArrayTo(this.data.samples, this.dataType, samples);
139
    if (!interleaved && this.fmt.numChannels > 1) {
140
      return deInterleave(samples, this.fmt.numChannels);
141
    }
142
    return samples;
143
  }
144
145
  /**
146
   * Return the sample at a given index.
147
   * @param {number} index The sample index.
148
   * @return {number} The sample.
149
   * @throws {Error} If the sample index is off range.
150
   */
151
  getSample(index) {
152
    index = index * (this.dataType.bits / 8);
153
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
154
      throw new Error('Range error');
155
    }
156
    return unpack(
157
      this.data.samples.slice(index, index + this.dataType.bits / 8),
158
      this.dataType);
159
  }
160
161
  /**
162
   * Set the sample at a given index.
163
   * @param {number} index The sample index.
164
   * @param {number} sample The sample.
165
   * @throws {Error} If the sample index is off range.
166
   */
167
  setSample(index, sample) {
168
    index = index * (this.dataType.bits / 8);
169
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
170
      throw new Error('Range error');
171
    }
172
    packTo(sample, this.dataType, this.data.samples, index);
173
  }
174
175
  /**
176
   * Return the value of the iXML chunk.
177
   * @return {string} The contents of the iXML chunk.
178
   */
179
  getiXML() {
180
    return this.iXML.value;
181
  }
182
183
  /**
184
   * Set the value of the iXML chunk.
185
   * @param {string} iXMLValue The value for the iXML chunk.
186
   * @throws {TypeError} If the value is not a string.
187
   */
188
  setiXML(iXMLValue) {
189
    if (typeof iXMLValue !== 'string') {
190
      throw new TypeError('iXML value must be a string.');
191
    }
192
    this.iXML.value = iXMLValue;
193
    this.iXML.chunkId = 'iXML';
194
  }
195
196
  /**
197
   * Get the value of the _PMX chunk.
198
   * @return {string} The contents of the _PMX chunk.
199
   */
200
  get_PMX() {
201
    return this._PMX.value;
202
  }
203
204
  /**
205
   * Set the value of the _PMX chunk.
206
   * @param {string} _PMXValue The value for the _PMX chunk.
207
   * @throws {TypeError} If the value is not a string.
208
   */
209
  set_PMX(_PMXValue) {
210
    if (typeof _PMXValue !== 'string') {
211
      throw new TypeError('_PMX value must be a string.');
212
    }
213
    this._PMX.value = _PMXValue;
214
    this._PMX.chunkId = '_PMX';
215
  }
216
217
  /**
218
   * Set up the WaveFileCreator object based on the arguments passed.
219
   * @param {number} numChannels The number of channels.
220
   * @param {number} sampleRate The sample rate.
221
   *    Integers like 8000, 44100, 48000, 96000, 192000.
222
   * @param {string} bitDepthCode The audio bit depth code.
223
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
224
   *    or any value between '8' and '32' (like '12').
225
   * @param {!Array|!TypedArray} samples The samples.
226
   * @param {?Object} options Optional. Used to force the container
227
   *    as RIFX with {'container': 'RIFX'}
228
   * @throws {Error} If any argument does not meet the criteria.
229
   * @private
230
   */
231
  newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
232
    if (!options.container) {
233
      options.container = 'RIFF';
234
    }
235
    this.container = options.container;
236
    this.bitDepth = bitDepthCode;
237
    samples = interleave(samples);
238
    this.updateDataType_();
239
    /** @type {number} */
240
    let numBytes = this.dataType.bits / 8;
241
    this.data.samples = new Uint8Array(samples.length * numBytes);
242
    packArrayTo(samples, this.dataType, this.data.samples);
243
    this.makeWavHeader_(
244
      bitDepthCode, numChannels, sampleRate,
245
      numBytes, this.data.samples.length, options);
246
    this.data.chunkId = 'data';
247
    this.data.chunkSize = this.data.samples.length;
248
    this.validateWavHeader_();
249
  }
250
251
  /**
252
   * Define the header of a wav file.
253
   * @param {string} bitDepthCode The audio bit depth
254
   * @param {number} numChannels The number of channels
255
   * @param {number} sampleRate The sample rate.
256
   * @param {number} numBytes The number of bytes each sample use.
257
   * @param {number} samplesLength The length of the samples in bytes.
258
   * @param {!Object} options The extra options, like container defintion.
259
   * @private
260
   */
261
  makeWavHeader_(
262
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
263
    if (bitDepthCode == '4') {
264
      this.createADPCMHeader_(
265
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
266
267
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
268
      this.createALawMulawHeader_(
269
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
270
271
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
272
        numChannels > 2) {
273
      this.createExtensibleHeader_(
274
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
275
276
    } else {
277
      this.createPCMHeader_(
278
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
279
    }
280
  }
281
282
  /**
283
   * Create the header of a linear PCM wave file.
284
   * @param {string} bitDepthCode The audio bit depth
285
   * @param {number} numChannels The number of channels
286
   * @param {number} sampleRate The sample rate.
287
   * @param {number} numBytes The number of bytes each sample use.
288
   * @param {number} samplesLength The length of the samples in bytes.
289
   * @param {!Object} options The extra options, like container defintion.
290
   * @private
291
   */
292
  createPCMHeader_(
293
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
294
    this.container = options.container;
295
    this.chunkSize = 36 + samplesLength;
296
    this.format = 'WAVE';
297
    this.bitDepth = bitDepthCode;
298
    this.fmt = {
299
      chunkId: 'fmt ',
300
      chunkSize: 16,
301
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
302
      numChannels: numChannels,
303
      sampleRate: sampleRate,
304
      byteRate: (numChannels * numBytes) * sampleRate,
305
      blockAlign: numChannels * numBytes,
306
      bitsPerSample: parseInt(bitDepthCode, 10),
307
      cbSize: 0,
308
      validBitsPerSample: 0,
309
      dwChannelMask: 0,
310
      subformat: []
311
    };
312
  }
313
314
  /**
315
   * Create the header of a ADPCM wave file.
316
   * @param {string} bitDepthCode The audio bit depth
317
   * @param {number} numChannels The number of channels
318
   * @param {number} sampleRate The sample rate.
319
   * @param {number} numBytes The number of bytes each sample use.
320
   * @param {number} samplesLength The length of the samples in bytes.
321
   * @param {!Object} options The extra options, like container defintion.
322
   * @private
323
   */
324
  createADPCMHeader_(
325
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
326
    this.createPCMHeader_(
327
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
328
    this.chunkSize = 40 + samplesLength;
329
    this.fmt.chunkSize = 20;
330
    this.fmt.byteRate = 4055;
331
    this.fmt.blockAlign = 256;
332
    this.fmt.bitsPerSample = 4;
333
    this.fmt.cbSize = 2;
334
    this.fmt.validBitsPerSample = 505;
335
    this.fact = {
336
      chunkId: 'fact',
337
      chunkSize: 4,
338
      dwSampleLength: samplesLength * 2
339
    };
340
  }
341
342
  /**
343
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
344
   * @param {string} bitDepthCode The audio bit depth
345
   * @param {number} numChannels The number of channels
346
   * @param {number} sampleRate The sample rate.
347
   * @param {number} numBytes The number of bytes each sample use.
348
   * @param {number} samplesLength The length of the samples in bytes.
349
   * @param {!Object} options The extra options, like container defintion.
350
   * @private
351
   */
352
  createExtensibleHeader_(
353
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
354
    this.createPCMHeader_(
355
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
356
    this.chunkSize = 36 + 24 + samplesLength;
357
    this.fmt.chunkSize = 40;
358
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
359
    this.fmt.cbSize = 22;
360
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
361
    this.fmt.dwChannelMask = dwChannelMask_(numChannels);
362
    // subformat 128-bit GUID as 4 32-bit values
363
    // only supports uncompressed integer PCM samples
364
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
365
  }
366
367
  /**
368
   * Create the header of mu-Law and A-Law wave files.
369
   * @param {string} bitDepthCode The audio bit depth
370
   * @param {number} numChannels The number of channels
371
   * @param {number} sampleRate The sample rate.
372
   * @param {number} numBytes The number of bytes each sample use.
373
   * @param {number} samplesLength The length of the samples in bytes.
374
   * @param {!Object} options The extra options, like container defintion.
375
   * @private
376
   */
377
  createALawMulawHeader_(
378
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
379
    this.createPCMHeader_(
380
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
381
    this.chunkSize = 40 + samplesLength;
382
    this.fmt.chunkSize = 20;
383
    this.fmt.cbSize = 2;
384
    this.fmt.validBitsPerSample = 8;
385
    this.fact = {
386
      chunkId: 'fact',
387
      chunkSize: 4,
388
      dwSampleLength: samplesLength
389
    };
390
  }
391
392
  /**
393
   * Set the string code of the bit depth based on the 'fmt ' chunk.
394
   * @private
395
   */
396
  bitDepthFromFmt_() {
397
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
398
      this.bitDepth = '32f';
399
    } else if (this.fmt.audioFormat === 6) {
400
      this.bitDepth = '8a';
401
    } else if (this.fmt.audioFormat === 7) {
402
      this.bitDepth = '8m';
403
    } else {
404
      this.bitDepth = this.fmt.bitsPerSample.toString();
405
    }
406
  }
407
408
  /**
409
   * Validate the bit depth.
410
   * @return {boolean} True is the bit depth is valid.
411
   * @throws {Error} If bit depth is invalid.
412
   * @private
413
   */
414
  validateBitDepth_() {
415
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
416
      if (parseInt(this.bitDepth, 10) > 8 &&
417
          parseInt(this.bitDepth, 10) < 54) {
418
        return true;
419
      }
420
      throw new Error('Invalid bit depth.');
421
    }
422
    return true;
423
  }
424
425
  /**
426
   * Update the type definition used to read and write the samples.
427
   * @private
428
   */
429
  updateDataType_() {
430
    this.dataType = {
431
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
432
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
433
      signed: this.bitDepth != '8',
434
      be: this.container == 'RIFX'
435
    };
436
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
437
      this.dataType.bits = 8;
438
      this.dataType.signed = false;
439
    }
440
  }
441
442
  /**
443
   * Validate the header of the file.
444
   * @throws {Error} If bit depth is invalid.
445
   * @throws {Error} If the number of channels is invalid.
446
   * @throws {Error} If the sample rate is invalid.
447
   * @ignore
448
   * @private
449
   */
450
  validateWavHeader_() {
451
    this.validateBitDepth_();
452
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
453
      throw new Error('Invalid number of channels.');
454
    }
455
    if (!validateSampleRate(
456
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
457
      throw new Error('Invalid sample rate.');
458
    }
459
  }
460
}
461
462
/**
463
 * Return the value for dwChannelMask according to the number of channels.
464
 * @param {number} numChannels the number of channels.
465
 * @return {number} the dwChannelMask value.
466
 * @private
467
 */
468
function dwChannelMask_(numChannels) {
469
  /** @type {number} */
470
  let mask = 0;
471
  // mono = FC
472
  if (numChannels === 1) {
473
    mask = 0x4;
474
  // stereo = FL, FR
475
  } else if (numChannels === 2) {
476
    mask = 0x3;
477
  // quad = FL, FR, BL, BR
478
  } else if (numChannels === 4) {
479
    mask = 0x33;
480
  // 5.1 = FL, FR, FC, LF, BL, BR
481
  } else if (numChannels === 6) {
482
    mask = 0x3F;
483
  // 7.1 = FL, FR, FC, LF, BL, BR, SL, SR
484
  } else if (numChannels === 8) {
485
    mask = 0x63F;
486
  }
487
  return mask;
488
}
489